package edu.northwestern.cbits.purple_robot_manager.probes.sample;
import java.util.Map;
import org.apache.commons.math3.complex.Complex;
import org.apache.commons.math3.transform.DftNormalization;
import org.apache.commons.math3.transform.FastFourierTransformer;
import org.apache.commons.math3.transform.TransformType;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import edu.northwestern.cbits.purple_robot_manager.R;
import edu.northwestern.cbits.purple_robot_manager.activities.settings.FlexibleListPreference;
import edu.northwestern.cbits.purple_robot_manager.logging.LogManager;
import edu.northwestern.cbits.purple_robot_manager.probes.Probe;
/**
* SampleProbe: This is a documented example of how to create new probes for
* Purple Robot. It accesses the device's built-in microphone to obtain audio
* samples and compute features from the samples.
*
* In addition to implementing this class, to register the new Probe with Purple
* Robot, the following steps must be completed:
*
* 1. In `res/values/arrays.xml`, add the following line to the `probe_classes`
* array:
*
* <item>sample.SampleProbe</item>
*
* This instructs Purple Robot to load the probe with the full classname
*
* edu.northwestern.cbits.purple_robot_manager.probes.sample.SampleProbe
*
* into the set of supported probes. The loader automatically provides the
*
* edu.northwestern.cbits.purple_robot_manager.probes
*
* package prefix.
*
* 2. In the ProbeManager.probeForName, add the following condition to the
* sequence of if-else statements:
*
* else if (probe instanceof SampleProbe) { SampleProbe sample = (SampleProbe)
* probe;
*
* if (sample.name(context).equalsIgnoreCase(name)) found = true; }
*
* This allows the system to determine if a probe is available that matches the
* given name. (This is an old & suboptimal mechanism that is due for
* replacement as time becomes available.)
*
* Once these steps are complete, the rest of the work involved is contained
* within this class file.
*/
public class SampleProbe extends Probe
{
public static final String NAME = "edu.northwestern.cbits.purple_robot_manager.probes.sample.SampleProbe";
private static final boolean DEFAULT_ENABLED = false;
private static final String ENABLE = "config_probe_sample_enabled";
private static final String FREQUENCY = "config_probe_sample_frequency";
private boolean _recording = false;
private final double[] _samples = new double[32768];
private long _lastCheck = 0;
@Override
public String getPreferenceKey() {
return "sample_sample";
}
/**
* Returns the machine-readable name of the probe used throughout the
* system. Typically is the full class name of the probe and defined as a
* class constant for use elsewhere in the system where a Context object may
* be unavailable.
*/
@Override
public String name(Context context)
{
return SampleProbe.NAME;
}
/**
* Returns a human-readable title for the probe that is used in various
* display contexts. Titles may be translated using the `strings.xml`
* mechanism.
*/
@Override
public String title(Context context)
{
return context.getString(R.string.title_sample_probe);
}
/**
* Returns a category name used to group probes in the app settings. Names
* should be defined in `strings.xml`, and not the source code for
* translations.
*/
@Override
public String probeCategory(Context context)
{
return context.getString(R.string.probe_misc_category);
}
/**
* Generates and returns a preferences screen for setting the options for
* the probe. The title and summary are shown in the probe lists. All probes
* should include an "Enable" checkbox to allow the user to deactivate the
* probe as needed. Additional optins such as frequencies and durations may
* also be defined. In the case below, a frequency parameter is defined that
* dictates how often the probe should emit a reading.
*/
@SuppressWarnings("deprecation")
@Override
public PreferenceScreen preferenceScreen(Context context, PreferenceManager manager)
{
PreferenceScreen screen = super.preferenceScreen(context, manager);
screen.setTitle(this.title(context));
screen.setSummary(this.summary(context));
CheckBoxPreference enabled = new CheckBoxPreference(context);
enabled.setTitle(R.string.title_enable_probe);
enabled.setKey(SampleProbe.ENABLE);
enabled.setDefaultValue(SampleProbe.DEFAULT_ENABLED);
screen.addPreference(enabled);
// Adding a frequency parameter that limits how frequently the probe can
// sample data...
FlexibleListPreference duration = new FlexibleListPreference(context);
duration.setKey(SampleProbe.FREQUENCY);
duration.setDefaultValue(Probe.DEFAULT_FREQUENCY);
duration.setEntryValues(R.array.probe_builtin_frequency_values);
duration.setEntries(R.array.probe_builtin_frequency_labels);
duration.setTitle(R.string.probe_frequency_label);
screen.addPreference(duration);
return screen;
}
/**
* Returns a human-readable summary that explains what the probe is and what
* it does.
*/
@Override
public String summary(Context context)
{
return context.getString(R.string.summary_sample_probe_desc);
}
/**
* Standard method called by various elements of the system to enable this
* probe.
*/
@Override
public void enable(Context context)
{
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
Editor e = prefs.edit();
e.putBoolean(SampleProbe.ENABLE, true);
e.commit();
}
/**
* Standard method called by various elements of the system to disable this
* probe.
*/
@Override
public void disable(Context context)
{
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
Editor e = prefs.edit();
e.putBoolean(SampleProbe.ENABLE, false);
e.commit();
}
/**
* Called by the system every fifteen seconds to verify if the probe is
* enabled and if there's work to do. Note that if a larger sample rate is
* required, probes must set up and manage their own threads that implement
* the desired sample rate.
*
* (The naming of this method needs to be revisited and revised.)
*
* This example will illustrate how to communicate with the device's
* built-in audio capture devices to calculate audio features from
* regularly-recorded audio samples.
*/
@Override
public boolean isEnabled(final Context context)
{
// First, check if probes are enabled across the app
// (Settings->Probes->Enable Probes)
if (super.isEnabled(context))
{
// Probes are enabled system-wide, continue...
// Fetch the SharedPreferences object of obtaining configuration
// parameters...
SharedPreferences prefs = Probe.getPreferences(context);
if (prefs.getBoolean(SampleProbe.ENABLE, SampleProbe.DEFAULT_ENABLED) == false)
{
// We're not enabled. Bailing out.
return false;
}
// Fetch the frequency settings and date to determine if enough time
// has elapsed
// since the last check...
long freq = Long.parseLong(prefs.getString(SampleProbe.FREQUENCY, Probe.DEFAULT_FREQUENCY));
long now = System.currentTimeMillis();
if (now - this._lastCheck < freq)
{
// Not enough time has passed. Bailing out...
return true;
}
this._lastCheck = now;
// If we're not recording, go ahead and get started...
if (this._recording == false)
{
this._recording = true;
// Since we are going to collect and analyze data in separate
// threads, create
// a final pointer to this instance of the probe so that we can
// still access
// functions like emitReading to send the data to the rest of
// the system after
// analysis is complete.
final SampleProbe me = this;
// Define what the probe should do in its own thread...
Runnable r = new Runnable()
{
@Override
@SuppressWarnings("deprecation")
public void run()
{
// Determine the size of the smallest buffer the probe
// will need
// to collect and store audio samples.
int bufferSize = AudioRecord.getMinBufferSize(44100, AudioFormat.CHANNEL_CONFIGURATION_MONO, AudioFormat.ENCODING_PCM_16BIT);
// Find the highest sample rate the device supports and
// create an
// AudioRecord instance for that rate...
AudioRecord recorder = null;
int[] rates = new int[]
{ 44100, 22050, 11025, 8000 };
for (int rate : rates)
{
if (recorder == null)
{
AudioRecord newRecorder = new AudioRecord(MediaRecorder.AudioSource.MIC, rate, AudioFormat.CHANNEL_CONFIGURATION_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize);
if (newRecorder.getState() == AudioRecord.STATE_INITIALIZED)
recorder = newRecorder;
else
newRecorder.release();
}
}
// If we were able to find a sample rate and create an
// AudioRecorder
// instance...
if (recorder != null)
{
// Create a buffer for samples and start recording.
// Note that the recording duration
// is a function of the sample rate and master
// buffer size (in seconds):
//
// _samples.length / recorder.getSampleRate
//
short[] buffer = new short[bufferSize];
recorder.startRecording();
int index = 0;
int read = 0;
// Designate a place to store a running tally for
// calculating
// the signal power...
double sampleSum = 0;
double samplePower = 0;
// While we have room in the master buffer and audio
// is available...
while (index < me._samples.length && 0 <= (read = recorder.read(buffer, 0, bufferSize)))
{
// Calculate the power as we collect samples...
for (int i = 0; i < read; i++)
{
if (index < me._samples.length)
{
sampleSum += Math.abs(buffer[i]);
samplePower += Math.pow(((double) buffer[i]) / Short.MAX_VALUE, 2);
me._samples[index] = (double) buffer[i];
index += 1;
}
}
}
// Master buffer is full, stop recording.
recorder.stop();
// Start building the data payload that the probe
// will emit. Note that the timestamp
// is in seconds at this level.
Bundle bundle = new Bundle();
bundle.putString("PROBE", me.name(context));
bundle.putLong("TIMESTAMP", System.currentTimeMillis() / 1000);
bundle.putInt("SAMPLE_RATE", recorder.getSampleRate());
bundle.putDouble("POWER", samplePower / me._samples.length);
bundle.putDouble("NORMALIZED_AVG_MAGNITUDE", (sampleSum / Short.MAX_VALUE) / me._samples.length);
// The AudioRecord instance is no longer needed at
// the moment, free it...
recorder.release();
// Let's calculate the magnitude and frequency of
// the audio signal using a
// Fast Fourier Transform. (Implementation provided
// by the Apache Commons Math
// library.)
//
// If you wanted to calculate your own features, you
// would replace the code below
// with your own code for analyzing the collected
// samples.
FastFourierTransformer fft = new FastFourierTransformer(DftNormalization.STANDARD);
Complex[] values = fft.transform(me._samples, TransformType.FORWARD);
double maxFrequency = 0;
double maxMagnitude = 0;
// Look at the possible wavelengths and determine
// which one has the largest magnitude...
for (int i = 0; i < values.length / 2; i++)
{
Complex value = values[i];
double magnitude = value.abs();
if (magnitude > maxMagnitude)
{
maxMagnitude = magnitude;
maxFrequency = (i * recorder.getSampleRate()) / (double) me._samples.length;
}
}
// Add the largest found frequency to the payload...
bundle.putDouble("FREQUENCY", maxFrequency);
// Transmit. Note that this is a thread-safe method,
// so probe data can be transmitted
// on a separate thread than the original one
// containing the isEnabled call.
me.transmitData(context, bundle);
}
// Wait a moment while data is flushed out of the system
// before we allow recording to
// resume...
try
{
Thread.sleep(10000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
// Reset the flag to allow recording to start in the
// next cycle.
me._recording = false;
}
};
// Start the thread.
Thread t = new Thread(r);
t.start();
return true;
}
return true;
}
return false;
}
/**
* Provides a human-readable display of the data in places like the main
* screen. A copy of the bundle transmitted is provided, and this method
* returns a string formatting the most relevant details.
*/
@Override
public String summarizeValue(Context context, Bundle bundle)
{
double freq = bundle.getDouble("FREQUENCY");
return String.format(context.getResources().getString(R.string.summary_audio_features_probe), freq);
}
@Override
public void updateFromMap(Context context, Map<String, Object> params)
{
super.updateFromMap(context, params);
if (params.containsKey(Probe.PROBE_FREQUENCY))
{
Object frequency = params.get(Probe.PROBE_FREQUENCY);
if ((frequency instanceof Double) == false)
frequency = Double.valueOf(frequency.toString()).longValue();
else
frequency = ((Double) frequency).longValue();
SharedPreferences prefs = Probe.getPreferences(context);
Editor e = prefs.edit();
e.putString(SampleProbe.FREQUENCY, frequency.toString());
e.commit();
}
}
@Override
public JSONObject fetchSettings(Context context)
{
JSONObject settings = super.fetchSettings(context);
try
{
JSONObject frequency = new JSONObject();
frequency.put(Probe.PROBE_TYPE, Probe.PROBE_TYPE_LONG);
JSONArray values = new JSONArray();
String[] options = context.getResources().getStringArray(R.array.probe_builtin_frequency_values);
for (String option : options)
{
values.put(Long.parseLong(option));
}
frequency.put(Probe.PROBE_VALUES, values);
settings.put(Probe.PROBE_FREQUENCY, frequency);
}
catch (JSONException e)
{
LogManager.getInstance(context).logException(e);
}
return settings;
}
@Override
public Map<String, Object> configuration(Context context)
{
Map<String, Object> map = super.configuration(context);
SharedPreferences prefs = Probe.getPreferences(context);
try
{
long freq = Long.parseLong(prefs.getString(SampleProbe.FREQUENCY, Probe.DEFAULT_FREQUENCY));
map.put(Probe.PROBE_FREQUENCY, freq);
}
catch (NumberFormatException e)
{
LogManager.getInstance(context).logException(e);
}
return map;
}
}